package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;

/**
 * @author Luka
 * @date 2021/06/02 14:26
 **/
public class Server {
    /**
     * 运行在服务端的SeverSocket主要有两个工作
     * 1:向系统申请服务端口,客户端就是通过这个端口与服务端建立连接的
     * 2:监听服务端口,一旦一个客户端通过该端口建立连接,服务端会自动创建一个Socket
     * 通过这个Socket与客户端交互
     *
     * 可以把Socket比喻成一个电话,SeverSocket则相当于是某呼叫中心的"总机"
     * 下面连着一堆电话
     */

    private ServerSocket server;
    /*
       该数组用于保存所有ClientHandler用于给客户端发送信息的输出流
       便于所有ClientHandler获取到其他ClientHandler的输出流来广播消息

       注:由于Server是ClientHandler的外部类,而内部类可以访问过外部类的成员,
       因此在Server上定义的数组可以被所有ClientHandler访问到,从而作为共享数
       据使用
    */
    private PrintWriter[] allout = {};


    public Server() {
        try {
            /*
                实例化ServerSocket的同时需要制定服务端口,客户端就是通过这个
                端口与服务端建立的
                指定的端口号不能与其他应用程序申请的一样,否则会抛一个异常:
                java.net.BindException:Address already in use
             */
            System.out.println("正在启动服务端...");
            server = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            /*
                ServerSocket提供了接受客户端连接的方法:
                Socket accept()
                该方法是一个阻塞方法,调用后服务端会"卡住",此时开始等待客户端的
                连接,一单一个客户端创建Socket并与服务端建立连接,那么这个方法会
                立即返回一个Socket,通过这个Socket就可以与客户端的Socket配对
                并进行双向通讯了
             */
            Socket socket;
            while (true) {
                System.out.println("等待客户端连接...");
                socket = server.accept();
                System.out.println("一个客户连接了!");

                /*
                    启动一个线程来处理与客户端的交互,是的主线程将该线程
                    启动后可以再次循环调用上面的accept方法等待其他客户端的连接
                 */
                //实例化线程任务
                ClientHandler handler = new ClientHandler(socket);
                //创建线程执行该任务
                Thread t = new Thread(handler);
                t.start();

            }

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务是负责与指定的客户端进行交互
     */

    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录客户端IP地址信息

        public ClientHandler(Socket socket) {
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }

        PrintWriter pw;
        public void run(){
            try {
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, "UTF-8");
                BufferedReader br = new BufferedReader(isr);

                 pw = new PrintWriter(
                        new BufferedWriter(
                                new OutputStreamWriter(
                                        socket.getOutputStream(),"UTF-8"
                                )
                        ),true
                );
                //将该pw放入allout数组,供其他ClientHandler使用一边广播消息
//                                synchronized (this) {//不行 因为this是ClientHandler
                //大部分情况下,锁定临近资源对象是可以的,只是下面会改变数组所以不行
//                                synchronized (allout) {//不行,因为同步块内部会改变allout只想(锁对象会发生改变)
                synchronized (Server.this) {
                    //1扩容allout
                    allout = Arrays.copyOf(allout,allout.length+1);
                    //将pw放入数组最后一个位置
                    allout[allout.length-1] = pw;
                    //广播某个用户上线
                }
                sendMessage(host+"上线了");

            /*
                这里使用BufferedReader按行读取客户端发送过来的每一行字符串
                时,如果客户端断开连接(客户端正常调用socket.close())那么
                readLine方法会返回null,表示流读取到了末尾
                如果客户端非正常断开(没有调用socket.close()或者强制结束程序)
                则哲理readLine方法会跑出异常:
                java.net.SocketException:Connection reset
             */
                while ((line = br.readLine()) != null) {
                    System.out.println(host+"说:" + line);
                    //将消息发给所有客户端
                    sendMessage(host+"说"+line);
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                //处理客户端断开连接后的操作

                //将对当前客户端发送信息的输出流从allout数组中删除
                synchronized (Server.this) {
                    for (int i = 0; i <allout.length ; i++) {
                        if (allout[i]==pw){
                            allout[i]=allout[allout.length-1];
                            allout = Arrays.copyOf(allout,allout.length-1);
                            break;
                        }
                    }
                }
                sendMessage(host+"下线了");

                try {
                    socket.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }

        /**
         * 将给定消息广播给所有客户端
         * @param message
         */
        String line;
        public void sendMessage(String message){
            synchronized (Server.this) {
                for (int i = 0; i <allout.length ; i++) {
                    allout[i].println(host+"说"+message);
                }
            }

        }
    }
}